如果物件內的方法,會依據物件內的狀態,使用多個 if - else if - else
或 switch case
來判定不同狀態下該執行的內容時,可以將 switch case
的邏輯抽取出多個獨立的物件,負責狀態的變化以及執行的內容。
通常發生在物件內的方法會依據自身的狀態,產生 if - else if - else
或 switch case
的邏輯判斷,如果判斷的情境數量太多會喪失程式的彈性,導致之後不易於維護以及修改。因此可以將所有的情境獨自轉換成單一物件,每個物件將執行各自的任務,而且還能夠在需要時更改物件的狀態,讓下次物件執行相同方法時,會自動執行新狀態下的相關程式碼。
作法是:
Context
)。State
物件),除了將原本邏輯判斷內實作細節的程式碼搬移過來,還可以依據需求,更改 Context
的狀態。Context
:
Context
的狀態更改為 State
物件。State
的方法。以下範例以「簡易型電視遊樂器」為核心製作。
製作獨立物件的虛擬層親代:State
public interface State {
public abstract void tryToShot();
public abstract void tryToKick();
public abstract void tryToJump();
public abstract void tryToMovingToRight();
public abstract void tryToMovingToLeft();
public abstract void tryToDoSomething();
public abstract void tryToBecomeSnipperMode();
public abstract void tryToBecomeStandingMode();
public abstract void tryToBecomeBoostMode();
}
製作具有多個邏輯判斷的物件:Hero
(Context 物件)
public class Hero {
private State state;
public Hero() {
this.state = new StandingState(this);
}
public void changeState(State state) {
this.state = state;
}
public State getState() {
return state;
}
public void tryToShot() {
state.tryToShot();
}
public void shot() {
System.out.println("英雄射出一發子彈");
}
public void tryToKick() {
state.tryToKick();
}
public void kick() {
System.out.println("英雄向前方踢了一腳");
}
public void tryToJump() {
state.tryToJump();
}
public void jump() {
System.out.println("英雄往上跳");
}
public void tryToMovingToRight() {
state.tryToMovingToRight();
}
public void moveToRight() {
System.out.println("英雄往右走");
}
public void tryToMovingToLeft() {
state.tryToMovingToLeft();
}
public void moveToLeft() {
System.out.println("英雄往左走");
}
public void tryToDoSomething() {
state.tryToDoSomething();
}
public void doNothing() {
System.out.println("什麼事也沒發生");
}
public void tryToBecomeSnipperMode() {
state.tryToBecomeSnipperMode();
}
public void tryToBecomeStandingMode() {
state.tryToBecomeStandingMode();
}
public void tryToBecomeBoostMode() {
state.tryToBecomeBoostMode();
}
}
製作獨立物件的子代:StandingState
、SnipperState
、BoostState
(State 物件)
public class StandingState implements State {
private Hero hero;
public StandingState(Hero hero) {
this.hero = hero;
}
@Override
public void tryToShot() {
hero.shot();
}
@Override
public void tryToKick() {
hero.kick();
}
@Override
public void tryToJump() {
hero.jump();
}
@Override
public void tryToMovingToRight() {
hero.moveToRight();
}
@Override
public void tryToMovingToLeft() {
hero.moveToLeft();
}
@Override
public void tryToDoSomething() {
hero.doNothing();
}
@Override
public void tryToBecomeSnipperMode() {
hero.changeState(new SnipperState(hero));
}
@Override
public void tryToBecomeStandingMode() {
hero.changeState(new StandingState(hero));
}
@Override
public void tryToBecomeBoostMode() {
hero.changeState(new BoostState(hero));
}
}
public class SnipperState implements State {
private Hero hero;
public SnipperState(Hero hero) {
this.hero = hero;
}
@Override
public void tryToShot() {
hero.shot();
}
@Override
public void tryToKick() {
hero.doNothing();
}
@Override
public void tryToJump() {
hero.doNothing();
}
@Override
public void tryToMovingToRight() {
hero.doNothing();
}
@Override
public void tryToMovingToLeft() {
hero.doNothing();
}
@Override
public void tryToDoSomething() {
hero.doNothing();
}
@Override
public void tryToBecomeSnipperMode() {
hero.changeState(new SnipperState(hero));
}
@Override
public void tryToBecomeStandingMode() {
hero.changeState(new StandingState(hero));
}
@Override
public void tryToBecomeBoostMode() {
hero.changeState(new BoostState(hero));
}
}
public class BoostState implements State {
private Hero hero;
public BoostState(Hero hero) {
this.hero = hero;
}
@Override
public void tryToShot() {
hero.doNothing();
}
@Override
public void tryToKick() {
hero.doNothing();
}
@Override
public void tryToJump() {
hero.jump();
}
@Override
public void tryToMovingToRight() {
hero.moveToRight();
}
@Override
public void tryToMovingToLeft() {
hero.moveToLeft();
}
@Override
public void tryToDoSomething() {
hero.doNothing();
}
@Override
public void tryToBecomeSnipperMode() {
hero.changeState(new BoostState(hero));
}
@Override
public void tryToBecomeStandingMode() {
hero.changeState(new StandingState(hero));
}
@Override
public void tryToBecomeBoostMode() {
hero.changeState(new BoostState(hero));
}
}
製作掌機的搖桿:VideoGameController
public class VideoGameController {
private Hero hero;
public VideoGameController(Hero hero) {
this.hero = hero;
}
public void pushButtonY() {
hero.tryToShot();
}
public void pushButtonX() {
hero.tryToKick();
}
public void pushButtonB() {
hero.tryToJump();
}
public void pushButtonA() {
hero.tryToDoSomething();
}
public void pushButtonRightArrow() {
hero.tryToMovingToRight();
}
public void pushButtonLeftArrow() {
hero.tryToMovingToLeft();
}
public void holdButtonR() {
hero.tryToBecomeSnipperMode();
}
public void releaseButtonR() {
hero.tryToBecomeStandingMode();
}
public void holdButtonL() {
hero.tryToBecomeBoostMode();
}
public void releaseButtonL() {
hero.tryToBecomeStandingMode();
}
}
測試,模擬玩家不斷在不同模式間嘗試按鈕:VideoGameStateSample
public class VideoGameStateSample {
public static void main(String[] args) {
Hero hero = new Hero();
VideoGameController controller = new VideoGameController(hero);
System.out.println("---嘗試基本按鈕---");
standardOperation(controller);
System.out.println("\n---按住按鈕 R---");
controller.holdButtonR();
standardOperation(controller);
System.out.println("\n---放開按鈕 R---");
controller.releaseButtonR();
standardOperation(controller);
System.out.println("\n---按住按鈕 L---");
controller.holdButtonL();
standardOperation(controller);
System.out.println("\n---放開按鈕 L---");
controller.releaseButtonL();
standardOperation(controller);
}
private static void standardOperation(VideoGameController controller) {
controller.pushButtonY();
controller.pushButtonX();
controller.pushButtonB();
controller.pushButtonA();
controller.pushButtonRightArrow();
controller.pushButtonLeftArrow();
}
}
製作獨立物件的虛擬層親代:State
/** @abstract */
class State {
/** @param {Hero} hero */
constructor(hero) {
this.hero = hero;
}
tryToShot() { return; }
tryToKick() { return; }
tryToJump() { return; }
tryToMovingToRight() { return; }
tryToMovingToLeft() { return; }
tryToDoSomething() { return; }
tryToBecomeSnipperMode() { return; }
tryToBecomeStandingMode() { return; }
tryToBecomeBoostMode() { return; }
}
製作具有多個邏輯判斷的物件:Hero
(Context 物件)
class Hero {
constructor() {
/** @type {State} */
this.state = new StandingState(this);
}
/** @param {State} state */
changeState(state) {
this.state = state;
}
getState() {
return this.state;
}
tryToShot() {
this.state.tryToShot();
}
shot() {
console.log("英雄射出一發子彈");
}
tryToKick() {
this.state.tryToKick();
}
kick() {
console.log("英雄向前方踢了一腳");
}
tryToJump() {
this.state.tryToJump();
}
jump() {
console.log("英雄往上跳");
}
tryToMovingToRight() {
this.state.tryToMovingToRight();
}
moveToRight() {
console.log("英雄往右走");
}
tryToMovingToLeft() {
this.state.tryToMovingToLeft();
}
moveToLeft() {
console.log("英雄往左走");
}
tryToDoSomething() {
this.state.tryToDoSomething();
}
doNothing() {
console.log("什麼事也沒發生");
}
tryToBecomeSnipperMode() {
this.state.tryToBecomeSnipperMode();
}
tryToBecomeStandingMode() {
this.state.tryToBecomeStandingMode();
}
tryToBecomeBoostMode() {
this.state.tryToBecomeBoostMode();
}
}
製作獨立物件的子代:StandingState
、SnipperState
、BoostState
(State 物件)
class StandingState extends State {
/** @param {Hero} hero */
constructor(hero) {
super(hero);
}
/** @override */
tryToShot() {
this.hero.shot();
}
/** @override */
tryToKick() {
this.hero.kick();
}
/** @override */
tryToJump() {
this.hero.jump();
}
/** @override */
tryToMovingToRight() {
this.hero.moveToRight();
}
/** @override */
tryToMovingToLeft() {
this.hero.moveToLeft();
}
/** @override */
tryToDoSomething() {
this.hero.doNothing();
}
/** @override */
tryToBecomeSnipperMode() {
this.hero.changeState(new SnipperState(this.hero));
}
/** @override */
tryToBecomeStandingMode() {
this.hero.changeState(new StandingState(this.hero));
}
/** @override */
tryToBecomeBoostMode() {
this.hero.changeState(new BoostState(this.hero));
}
}
class SnipperState extends State {
/** @param {Hero} hero */
constructor(hero) {
super(hero);
}
/** @override */
tryToShot() {
this.hero.shot();
}
/** @override */
tryToKick() {
this.hero.doNothing();
}
/** @override */
tryToJump() {
this.hero.doNothing();
}
/** @override */
tryToMovingToRight() {
this.hero.doNothing();
}
/** @override */
tryToMovingToLeft() {
this.hero.doNothing();
}
/** @override */
tryToDoSomething() {
this.hero.doNothing();
}
/** @override */
tryToBecomeSnipperMode() {
this.hero.changeState(new SnipperState(this.hero));
}
/** @override */
tryToBecomeStandingMode() {
this.hero.changeState(new StandingState(this.hero));
}
/** @override */
tryToBecomeBoostMode() {
this.hero.changeState(new BoostState(this.hero));
}
}
class BoostState extends State {
/** @param {Hero} hero */
constructor(hero) {
super(hero);
}
/** @override */
tryToShot() {
this.hero.doNothing();
}
/** @override */
tryToKick() {
this.hero.doNothing();
}
/** @override */
tryToJump() {
this.hero.jump();
}
/** @override */
tryToMovingToRight() {
this.hero.moveToRight();
}
/** @override */
tryToMovingToLeft() {
this.hero.moveToLeft();
}
/** @override */
tryToDoSomething() {
this.hero.doNothing();
}
/** @override */
tryToBecomeSnipperMode() {
this.hero.changeState(new BoostState(this.hero));
}
/** @override */
tryToBecomeStandingMode() {
this.hero.changeState(new StandingState(this.hero));
}
/** @override */
tryToBecomeBoostMode() {
this.hero.changeState(new BoostState(this.hero));
}
}
製作掌機的搖桿:VideoGameController
class VideoGameController {
/** @param {Hero} hero */
constructor(hero) {
this.hero = hero;
}
pushButtonY() {
this.hero.tryToShot();
}
pushButtonX() {
this.hero.tryToKick();
}
pushButtonB() {
this.hero.tryToJump();
}
pushButtonA() {
this.hero.tryToDoSomething();
}
pushButtonRightArrow() {
this.hero.tryToMovingToRight();
}
pushButtonLeftArrow() {
this.hero.tryToMovingToLeft();
}
holdButtonR() {
this.hero.tryToBecomeSnipperMode();
}
releaseButtonR() {
this.hero.tryToBecomeStandingMode();
}
holdButtonL() {
this.hero.tryToBecomeBoostMode();
}
releaseButtonL() {
this.hero.tryToBecomeStandingMode();
}
}
測試,模擬玩家不斷在不同模式間嘗試按鈕:VideoGameStateSample
/** @param {VideoGameController} controller */
const standardOperation = (controller) => {
controller.pushButtonY();
controller.pushButtonX();
controller.pushButtonB();
controller.pushButtonA();
controller.pushButtonRightArrow();
controller.pushButtonLeftArrow();
};
const videoGameStateSample = () => {
const hero = new Hero();
const controller = new VideoGameController(hero);
console.log("---嘗試基本按鈕---");
standardOperation(controller);
console.log("\n---按住按鈕 R---");
controller.holdButtonR();
standardOperation(controller);
console.log("\n---放開按鈕 R---");
controller.releaseButtonR();
standardOperation(controller);
console.log("\n---按住按鈕 L---");
controller.holdButtonL();
standardOperation(controller);
console.log("\n---放開按鈕 L---");
controller.releaseButtonL();
standardOperation(controller);
};
videoGameStateSample();
State 模式有趣的地方,在於要改善方法內因為過多的 if - else if - else
或 switch case
導致程式碼過多的情況,因為過多的程式碼通常暗示著「難以改動」的可能,違反追求高彈性、易於修改的理念。
實作時觀察到有三點要多加注意:
State
的虛擬層規範的方法足以用於所有的 State
?Context
在呼叫方法時,可以連帶更改自己的狀態?然而,就我自己的開發經驗,使用的 if - else if - else
或 switch case
不一定是糟糕的做法,假如邏輯判斷的數量只有幾個的話,反而不用特地抽離、製作成 State
物件,避免會增加開發上的麻煩。
講來講去可以發現,什麼情況下需要轉換,會是更重要的問題。
我想一個好的開發者如果能了解現在以及往後的可能需求,將更有把握判斷要不要轉換。
最後,嘗試使用後明顯感受到,那些依賴狀態的變換而有不同程式碼要執行的方法們,不用再寫一大堆判斷了,單單一、兩行就做到相同的事情,這感覺真棒!
明天將介紹 Behavioural patterns 的第九個模式:Strategy 模式。